package com.my.commons.utils.concurrent;

import java.io.PrintStream;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.log4j.Logger;

@SuppressWarnings("unchecked")
public final class RunnableStatsManager {
	/**
     * Logger for this class
     */
    private static final Logger log = Logger.getLogger(RunnableStatsManager.class);

    private static final Map<Class<?>, ClassStat> classStats = new HashMap<Class<?>, ClassStat>();

    private static final class ClassStat {
        private final String className;
        private final MethodStat runnableStat;

        private String[] methodNames = new String[0];
        private MethodStat[] methodStats = new MethodStat[0];

        private ClassStat(Class<?> clazz) {
            className = clazz.getName().replace("com.aionemu.gameserver.", "");
            runnableStat = new MethodStat(className, "run()");

            methodNames = new String[]{"run()"};
            methodStats = new MethodStat[]{runnableStat};

            classStats.put(clazz, this);
        }

        private MethodStat getRunnableStat() {
            return runnableStat;
        }

        private MethodStat getMethodStat(String methodName, boolean synchronizedAlready) {
            // method names will be interned automatically because of compiling, so this gonna work
            if (methodName == "run()")
                return runnableStat;

            for (int i = 0; i < methodNames.length; i++)
                if (methodNames[i].equals(methodName))
                    return methodStats[i];

            if (!synchronizedAlready) {
                synchronized (this) {
                    return getMethodStat(methodName, true);
                }
            }

            methodName = methodName.intern();

            final MethodStat methodStat = new MethodStat(className, methodName);

            methodNames = (String[]) ArrayUtils.add(methodNames, methodName);
            methodStats = (MethodStat[]) ArrayUtils.add(methodStats, methodStat);

            return methodStat;
        }
    }

    private static final class MethodStat {
        private final ReentrantLock lock = new ReentrantLock();

        private final String className;
        private final String methodName;

        private long count;
        private long total;
        private long min = Long.MAX_VALUE;
        private long max = Long.MIN_VALUE;

        private MethodStat(String className, String methodName) {
            this.className = className;
            this.methodName = methodName;
        }

        private void handleStats(long runTime) {
            lock.lock();
            try {
                count++;
                total += runTime;
                min = Math.min(min, runTime);
                max = Math.max(max, runTime);
            }
            finally {
                lock.unlock();
            }
        }
    }

    private static ClassStat getClassStat(Class<?> clazz, boolean synchronizedAlready) {
        ClassStat classStat = classStats.get(clazz);

        if (classStat != null)
            return classStat;

        if (!synchronizedAlready) {
            synchronized (RunnableStatsManager.class) {
                return getClassStat(clazz, true);
            }
        }

        return new ClassStat(clazz);
    }

    public static void handleStats(Class<? extends Runnable> clazz, long runTime) {
        getClassStat(clazz, false).getRunnableStat().handleStats(runTime);
    }

    public static void handleStats(Class<?> clazz, String methodName, long runTime) {
        getClassStat(clazz, false).getMethodStat(methodName, false).handleStats(runTime);
    }

    public static enum SortBy {
        AVG("average"),
        COUNT("count"),
        TOTAL("total"),
        NAME("class"),
        METHOD("method"),
        MIN("min"),
        MAX("max"),;

        private final String xmlAttributeName;

        private SortBy(String xmlAttributeName) {
            this.xmlAttributeName = xmlAttributeName;
        }

        private final Comparator<MethodStat> comparator = new Comparator<MethodStat>() {
            @SuppressWarnings("rawtypes")
            public int compare(MethodStat o1, MethodStat o2) {
                final Comparable c1 = getComparableValueOf(o1);
                final Comparable c2 = getComparableValueOf(o2);

                if (c1 instanceof Number)
                    return c2.compareTo(c1);

                final String s1 = (String) c1;
                final String s2 = (String) c2;

                final int len1 = s1.length();
                final int len2 = s2.length();
                final int n = Math.min(len1, len2);

                for (int k = 0; k < n; k++) {
                    char ch1 = s1.charAt(k);
                    char ch2 = s2.charAt(k);

                    if (ch1 != ch2) {
                        if (Character.isUpperCase(ch1) != Character
                                .isUpperCase(ch2))
                            return ch2 - ch1;
                        else
                            return ch1 - ch2;
                    }
                }

                final int result = len1 - len2;

                if (result != 0)
                    return result;

                switch (SortBy.this) {
                    case METHOD:
                        return NAME.comparator.compare(o1, o2);
                    default:
                        return 0;
                }
            }
        };

        @SuppressWarnings("rawtypes")
        private Comparable getComparableValueOf(MethodStat stat) {
            switch (this) {
                case AVG:
                    return stat.total / stat.count;
                case COUNT:
                    return stat.count;
                case TOTAL:
                    return stat.total;
                case NAME:
                    return stat.className;
                case METHOD:
                    return stat.methodName;
                case MIN:
                    return stat.min;
                case MAX:
                    return stat.max;
                default:
                    throw new InternalError();
            }
        }

        private static final SortBy[] VALUES = SortBy.values();
    }

    public static void dumpClassStats() {
        dumpClassStats(null);
    }

    @SuppressWarnings("rawtypes")
    public static void dumpClassStats(final SortBy sortBy) {
        final List<MethodStat> methodStats = new ArrayList<MethodStat>();

        synchronized (RunnableStatsManager.class) {
            for (ClassStat classStat : classStats.values())
                for (MethodStat methodStat : classStat.methodStats)
                    if (methodStat.count > 0)
                        methodStats.add(methodStat);
        }

        if (sortBy != null)
            Collections.sort(methodStats, sortBy.comparator);

        final List<String> lines = new ArrayList<String>();

        lines.add("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>");
        lines.add("<entries>");
        lines.add("\t<!-- This XML contains statistics about execution times. -->");
        lines.add("\t<!-- Submitted results will help the developers to optimize the server. -->");

        final String[][] values = new String[SortBy.VALUES.length][methodStats.size()];
        final int[] maxLength = new int[SortBy.VALUES.length];

        for (int i = 0; i < SortBy.VALUES.length; i++) {
            final SortBy sort = SortBy.VALUES[i];

            for (int k = 0; k < methodStats.size(); k++) {
                final Comparable c = sort.getComparableValueOf(methodStats.get(k));

                final String value;

                if (c instanceof Number)
                    value = NumberFormat.getInstance(Locale.ENGLISH).format(((Number) c).longValue());
                else
                    value = String.valueOf(c);

                values[i][k] = value;

                maxLength[i] = Math.max(maxLength[i], value.length());
            }
        }

        for (int k = 0; k < methodStats.size(); k++) {
            StringBuilder sb = new StringBuilder();
            sb.append("\t<entry ");

            EnumSet<SortBy> set = EnumSet.allOf(SortBy.class);

            if (sortBy != null) {
                switch (sortBy) {
                    case NAME:
                    case METHOD:
                        appendAttribute(sb, SortBy.NAME, values[SortBy.NAME.ordinal()][k], maxLength[SortBy.NAME
                                .ordinal()]);
                        set.remove(SortBy.NAME);

                        appendAttribute(sb, SortBy.METHOD, values[SortBy.METHOD.ordinal()][k], maxLength[SortBy.METHOD
                                .ordinal()]);
                        set.remove(SortBy.METHOD);
                        break;
                    default:
                        appendAttribute(sb, sortBy, values[sortBy.ordinal()][k], maxLength[sortBy.ordinal()]);
                        set.remove(sortBy);
                        break;
                }
            }

            for (SortBy sort : SortBy.VALUES)
                if (set.contains(sort))
                    appendAttribute(sb, sort, values[sort.ordinal()][k], maxLength[sort.ordinal()]);

            sb.append("/>");

            lines.add(sb.toString());
        }

        lines.add("</entries>");

        PrintStream ps = null;
        try {
            ps = new PrintStream("MethodStats-" + System.currentTimeMillis() + ".log");

            for (String line : lines)
                ps.println(line);
        }
        catch (Exception e) {
            log.warn("", e);
        }
        finally {
            IOUtils.closeQuietly(ps);
        }
    }

    private static void appendAttribute(StringBuilder sb, SortBy sortBy, String value, int fillTo) {
        sb.append(sortBy.xmlAttributeName);
        sb.append("=");

        if (sortBy != SortBy.NAME && sortBy != SortBy.METHOD)
            for (int i = value.length(); i < fillTo; i++)
                sb.append(" ");

        sb.append("\"");
        sb.append(value);
        sb.append("\" ");

        if (sortBy == SortBy.NAME || sortBy == SortBy.METHOD)
            for (int i = value.length(); i < fillTo; i++)
                sb.append(" ");
    }
}
